package webserver;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;

/**
 * Class representing a HTTP/1.0 Request with all necessary information
 * 
 */
// <editor-fold defaultstate="collapsed" desc=" UML Marker "> 
// #[regen=yes,id=DCE.C5433613-C438-29AE-0390-61A711E95AA3]
// </editor-fold> 
public class HTTP10Request extends Request {

	// Static Declarations
	/**
	 * SimpleDateFormat specification to generate the format of LastModified Header
	 */
	protected static final SimpleDateFormat FORMAT_LASTMODIFIED = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
	/**
	 * SimpleDateFormat specification to generate the timestamp format to use with combined logfile format
	 */
	protected static final SimpleDateFormat FORMAT_LOGFILE = new SimpleDateFormat("[dd/MMM/yyyy:HH:mm:ss Z]", Locale.US);
	private static final String HEADER_NOTIMPLEMENTED = "HTTP/1.0 501 Not Implemented\r\n\r\n";
	private static final String HEADER_FILENOTFOUND = "HTTP/1.0 404 Not Found\r\n\r\n";
	private static final String HEADER_SUCCESSFUL = "HTTP/1.0 200 OK\r\n";
	private static final String HEADER_BAD_REQUEST = "HTTP/1.0 400 Bad Request\r\n\r\n";

	// Variables
	/**
	 * Representing the requestType of a HTTP-Request (GET, POST, HEAD) otherwise NOTIMPLEMENTED
	 */
	public enum requestTypeEnum {

		GET, POST, HEAD, NOTIMPLEMENTED
	};
	/**
	 * Representing the requestType of this Request
	 */
	protected requestTypeEnum requestType;
	/**
	 * The status code for the response sent after this Request (generated when calling generateResponse())
	 * @see HTTP10Request#generateResponse()
	 */
	protected int statuscode;
	/**
	 * Storing the header entries for this Request as (key, value) pairs
	 */
	protected HashMap<String, String> requestHeader;
	/**
	 * The length of the content (bytes) sent as Response
	 */
	protected int length;
	/**
	 * Storing the data submitted with the Request
	 */
	protected ArrayList<String> requestData;
	private boolean gzip;
	/**
	 * The time to wait until a Socket can be closed
	 */
	protected int wait_time = -1;
	/**
	 * Should the requested file be recached
	 */
	protected boolean recache = false;

	/**
	 * HTTP10Request Default Constructor
	 * @param input - The input
	 */
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,id=DCE.ACD019F5-EA1A-AA73-F861-33F22101A30E]
	// </editor-fold>
	public HTTP10Request(ArrayList<String> input) {
		super(input);

		this.requestHeader = new HashMap<String, String>();
		this.requestData = new ArrayList<String>();
		this.keepalive = false;
		this.gzip = false;
	}

	/**
	 * Parse input and get needed data
	 * @throws IllegalStateException
	 */
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,id=DCE.0092D2E9-A0CF-8C76-EA19-FC7AE9F5B4B8]
	// </editor-fold>
	@Override
	public void process() throws IllegalStateException {
		readStateEnum state = readStateEnum.HEADER;
		String lastHeaderLine = "";

		try {
			for (String line : this.input) {
				// Parse Header Information
				if (state == readStateEnum.HEADER) {
					// Header is at end -> Data section starts now
					if (line.isEmpty()) {
						state = readStateEnum.DATA;
					} else {
						// Line is an extension to the previously found line
						if ((line.charAt(0) == '\t' || line.charAt(0) == ' ') && !lastHeaderLine.isEmpty()) {
							String last = this.requestHeader.get(lastHeaderLine);
							this.requestHeader.remove(lastHeaderLine);
							this.requestHeader.put(lastHeaderLine, last + " " + line.trim());
							continue;
						}

						String[] splitted = line.split(":");

						// First line found (methodename  ressourcenpfad  httpversion)
						if (splitted.length == 1) {
							splitted = line.split(" ");

							// Check if the Request-Type is known
							try {
								this.requestType = requestTypeEnum.valueOf(splitted[0].trim());
							} catch (IllegalArgumentException arg) {
								Logger.writeToLog("Found unknown Request-Type: " + splitted[0].trim());
								this.requestType = requestTypeEnum.NOTIMPLEMENTED;
							}
							this.relPath = splitted[1].trim().replace("%20", " ");

							// Normal Header line found
						} else if (splitted.length == 2) {
							this.requestHeader.put(splitted[0].trim(), splitted[1].trim());
							lastHeaderLine = splitted[0].trim();
						} else {
							StringBuffer value = new StringBuffer(splitted[1]);
							for (int i = 2; i < splitted.length; i++) {
								value.append(":");
								value.append(splitted[i]);
							}
							this.requestHeader.put(splitted[0].trim(), value.toString().trim());
							lastHeaderLine = splitted[0].trim();
						}
					}

					// Parsing Request Data
				} else {
					this.requestData.add(line);
				}
			}

			if (state != readStateEnum.DATA) {
				throw new IllegalStateException("End of Header block could not be recognized during parsing!");
			}

			// Check Keep-Alive
			if (this.requestHeader.containsKey("Connection") && this.requestHeader.get("Connection").equals("Keep-Alive")) {
				this.keepalive = true;
			}

			// Check Keep-Alive Timeout
			if (this.requestHeader.containsKey("Keep-Alive")) {
				String[] temp = this.requestHeader.get("Keep-Alive").split(" ");
				this.wait_time = Integer.parseInt(temp[0]);
			}

			// Check if GZip is supported
			if (this.requestHeader.containsKey("Accept-Encoding") && this.requestHeader.get("Accept-Encoding").contains("gzip")) {
				this.gzip = true;
			}

			// Check for Recaching (Pragma)
			if ((this.requestHeader.containsKey("Pragma") && this.requestHeader.get("Pragma").contains("no-cache"))
					|| (this.requestHeader.containsKey("Cache-Control") && this.requestHeader.get("Cache-Control").equals("no-cache"))) {
				this.recache = true;
			}
		} catch (IllegalStateException ise) {
			Logger.writeToLog("Bad Request: " + ise.toString());
			this.statuscode = 400;
			this.header = new byte[HEADER_BAD_REQUEST.length()];
			this.header = HEADER_BAD_REQUEST.getBytes();
			this.data = Cache.getInstance().getErrorPage(400);
		}
	}

	/**
	 * Get the length
	 * @return int
	 */
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,regenBody=yes,id=DCE.DF814F52-0F5D-1EDA-89B9-310FB84DBB2F]
	// </editor-fold>
	public int getLength() {
		return length;
	}

	/**
	 * Get all header lines
	 * @return HashMap<String,String> - key/value-Pairs
	 */
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,regenBody=yes,id=DCE.8F4A82D5-94D3-86CB-3FE0-853CDCCF5DD5]
	// </editor-fold>
	public HashMap<String, String> getRequestHeader() {
		return requestHeader;
	}

	/**
	 * Get the Request Type
	 * @return requestTypeEnum
	 */
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,regenBody=yes,id=DCE.D82CF4BA-C9B6-449A-1F59-FE844ED7A263]
	// </editor-fold>
	public requestTypeEnum getRequestType() {
		return requestType;
	}

	/**
	 * Get the StatusCode
	 * @return int
	 */
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,regenBody=yes,id=DCE.50C0936C-C0B1-0422-FAC9-B919C2EC793F]
	// </editor-fold>
	public int getStatuscode() {
		return statuscode;
	}

	/**
	 * Return a String representation of this Request
	 * @return String
	 */
	@Override
	public String toString() {
		return super.toString() + " Request-Type: " + this.requestType.toString();
	}

	@Override
	public void generateResponse() throws IllegalStateException {

		// Header has been already set by another function
		if (this.header != null) {
			return;
		}

		// Not Implemented
		if (this.requestType == requestTypeEnum.NOTIMPLEMENTED) {
			this.statuscode = 501;
			this.header = new byte[HEADER_NOTIMPLEMENTED.length()];
			this.header = HEADER_NOTIMPLEMENTED.getBytes();
			this.data = Cache.getInstance().getErrorPage(501);
			return;
		}

		// Get Request
		if (this.requestType == requestTypeEnum.GET) {
			byte[] dataT = Cache.getInstance().getFileContents(AppProperties.getInstance().getDefaultDomain() + "\\" + this.relPath, this.recache);
			CacheItem item = Cache.getInstance().getCacheItem(this.relPath);

			// File not found
			if (dataT == null || dataT.length == 0) {
				this.statuscode = 404;
				this.header = new byte[HEADER_FILENOTFOUND.length()];
				this.header = HEADER_FILENOTFOUND.getBytes();
				this.data = Cache.getInstance().getErrorPage(404);
				return;
			}

			// 200 - Successful
			this.statuscode = 200;
			String additionalHeader = this.generateHeaderItems(item);
			this.header = new byte[HEADER_SUCCESSFUL.length() + additionalHeader.length()];
			this.header = (HEADER_SUCCESSFUL + additionalHeader).getBytes();
			this.data = dataT;
			return;
		}

		// Post Request
		if (this.requestType == requestTypeEnum.POST) {
			File test = new File(AppProperties.getInstance().getHomeDirectory() + "\\" + AppProperties.getInstance().getDefaultDomain() + "\\" + this.relPath);

			// 404 - File not Found
			if (!test.exists()) {
				this.statuscode = 404;
				this.header = new byte[HEADER_FILENOTFOUND.length()];
				this.header = HEADER_FILENOTFOUND.getBytes();
				this.data = Cache.getInstance().getErrorPage(404);
				return;
			}

			// 200 - Successful
			this.statuscode = 200;
			String additionalHeader = this.generateHeaderItems(null);
			this.header = new byte[HEADER_SUCCESSFUL.length() + additionalHeader.length()];
			this.header = (HEADER_SUCCESSFUL + additionalHeader).getBytes();
			String tData = this.requestData.toString();
			this.data = new byte[tData.length()];
			this.data = tData.getBytes();
			return;
		}

		// Head Request
		if (this.requestType == requestTypeEnum.HEAD) {
			byte[] dataT = Cache.getInstance().getFileContents(AppProperties.getInstance().getDefaultDomain() + "\\" + this.relPath, this.recache);
			CacheItem item = Cache.getInstance().getCacheItem(AppProperties.getInstance().getDefaultDomain() + "\\" + this.relPath);

			// File not found
			if (dataT == null) {
				this.statuscode = 404;
				this.header = new byte[HEADER_FILENOTFOUND.length()];
				this.header = HEADER_FILENOTFOUND.getBytes();
				this.data = Cache.getInstance().getErrorPage(404);
				return;
			}

			// 200 - Successful
			this.statuscode = 200;
			String additionalHeader = this.generateHeaderItems(item);
			this.header = new byte[HEADER_SUCCESSFUL.length() + additionalHeader.length()];
			this.header = (HEADER_SUCCESSFUL + additionalHeader).getBytes();
			return;
		}

		//throw new IllegalStateException("This point should not be reached in generateResponse()!");
	}

	/**
	 * Generate additional Header Items for Response
	 * @param item - CacheItem (can be null)
	 * @return String
	 */
	protected String generateHeaderItems(CacheItem item) {
		StringBuffer fullHeader = new StringBuffer();

		// General Headers
		fullHeader.append("Server: DHBW JavaWebServer_BriemBesslerRausch/0.9\r\n"); // Add Server Header
		//fullHeader.append("Location: " + getAbsHttpPath() + "/\r\n"); // Add Location Header
		fullHeader.append("Date: " + FORMAT_LASTMODIFIED.format(new java.util.Date().getTime()) + "\r\n"); // Add Date Header

		if (this.keepalive) {
			fullHeader.append("Connection: keep-alive\r\n"); // Set Keep-Alive
		} else {
			fullHeader.append("Connection: close\r\n");
		}

		// File specific Headers (only generated if a Response with data will be sent)
		if (item != null) {
			if (item.lastModified().getTime() > 24 * 60 * 60) {
				fullHeader.append("Last Modified: " + FORMAT_LASTMODIFIED.format(item.lastModified()) + "\r\n");
			} // Add Last-Modified Timestamp
			fullHeader.append("Content-Type: " + item.getContentType() + "\r\n"); // Add MIME-Type of content
			fullHeader.append("Content-Length: " + item.getFileSize() + "\r\n"); // Add Lentgh of content
		}

		fullHeader.append("\r\n"); // Add trailing newline

		return fullHeader.toString();
	}

	/**
	 * Get the absolut HTTP path to a requested file
	 * @return String
	 */
	protected String getAbsHttpPath() {
		return "http://" + AppProperties.getInstance().getDefaultDomain() + this.relPath;
	}

	/**
	 * Generate logfile entry for Combined Logfile Format
	 * @return String
	 */
	@Override
	public String getLogFileEntry() {
		StringBuffer ret = new StringBuffer(this.socket.getInetAddress().toString()); // Host
		ret.append("\t-"); // User
		ret.append("\t-"); // Password
		ret.append("\t" + FORMAT_LOGFILE.format(new java.util.Date().getTime())); // Timestamp
		ret.append("\t\"" + this.requestType.toString() + " " + this.relPath + " HTTP/1.0\""); // Request header
		ret.append("\t" + this.statuscode); // Statuscode
		try {
			ret.append("\t" + this.data.length); // Byte count
		} catch(NullPointerException ex) {
			ret.append("\t");
		}
		ret.append("\t-"); // Referer
		if (this.requestHeader.containsKey("User-Agent")) {
			ret.append("\t\"" + this.requestHeader.get("User-Agent") + "\""); // User-Agent
		} else {
			ret.append("\t-");
		}

		return ret.toString();
	}

	/**
	 * Get the Timeout when a persisten connection can be closed
	 * @return int
	 */
	@Override
	public int getPersistentTimeout() {
		return this.wait_time;
	}
}

